/*
 * Copyright 2019 Purushottam Pawar
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.prush.bndrsntchtimer;

import ohos.aafwk.ability.Lifecycle;
import ohos.aafwk.ability.LifecycleObserver;
import ohos.aafwk.ability.LifecycleStateObserver;

import ohos.aafwk.content.Intent;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;

import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;

import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;

import ohos.app.Context;

import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;

/**
 * A horizontal progress bar shrinking with time; similar to Bandersnatch choice interface.
 */
public class BndrsntchTimer extends Component implements LifecycleStateObserver, Component.DrawTask {
    private static final String TAG = "BndrsntchTimer";
    private static final int RESET_ANIM_DURATION = 1000;

    private static final int DEFAULT_STROKE_WIDTH = 2;
    private static final int DEFAULT_HEIGHT = 16;
    private static final int DEFAULT_ROUND_RECT_RADIUS = 0;
    private static final float DEFAULT_PADDING_FACTOR = 30.0f;

    private int mRoundRectRadius = DEFAULT_ROUND_RECT_RADIUS;
    private int mTimerHeight = DEFAULT_HEIGHT;
    private long mTimerDuration;
    private int mLeftXPosition;
    private int mFactor;

    private RectFloat mRectF;
    private Paint mBackgroundPaint;
    private OnTimerElapsedListener mOnTimerElapsedListener;
    private AnimatorValue mTransformValueAnimator;
    private long mCurrentPlayTime;
    private boolean mbViewVisible;
    private boolean mbTimerElapsed;
    private Color mProgressColor;

    /**
     * Callback to be invoked when Timer is elaspsed.
     */
    public interface OnTimerElapsedListener {
        /**
         * Notifies the implementer when timer has elapsed.
         *
         * @param elapsedDuration long elapsed duration in millis
         * @param totalDuration   long    total duration in millis
         */
        void onTimeElapsed(final long elapsedDuration, final long totalDuration);
    }

    /**
     * Callback to be invoked for the Timer reset.
     */
    public interface OnTimerResetListener {
        /**
         * Notifies the implementer when timer reset has completed.
         */
        void onTimerResetCompleted();
    }

    public BndrsntchTimer(Context context) {
        super(context);
        init();
    }

    public BndrsntchTimer(Context context, AttrSet attrSet) {
        this(context, attrSet, 0);
        initializeAttrs(attrSet);
        init();
    }

    public BndrsntchTimer(Context context, AttrSet attrSet, int resId) {
        super(context, attrSet, resId);
        initializeAttrs(attrSet);
        init();
    }

    private void initializeAttrs(AttrSet attrSet) {
        mProgressColor = attrSet.getAttr("progress_color").isPresent() ?
                attrSet.getAttr("progress_color").get().getColorValue() : Color.WHITE;
    }

    private void init() {
        mRectF = new RectFloat();
        setTimerElapsed(true);
        initPaint();
        addDrawTask(this::onDraw);
    }

    private void initPaint() {
        mBackgroundPaint = new Paint();
        mBackgroundPaint.setAntiAlias(true);
        mBackgroundPaint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
        mBackgroundPaint.setColor(mProgressColor);
    }

    /**
     * Sets the color for the progress indicator.
     *
     * @param progressColor the color of the progress indicator of {@link BndrsntchTimer}
     */
    public void setProgressColor(final Color progressColor) {
        mProgressColor = progressColor;
        initPaint();
    }

    /**
     * Get the timer duration.
     * @return long value having the timer duration
     */
    public long getTimerDuration() {
        return mTimerDuration;
    }

    private void setTimerDuration(long timerDuration) {
        mTimerDuration = timerDuration;
    }

    private void setTimerElapsed(boolean bTimerElapsed) {
        this.mbTimerElapsed = bTimerElapsed;
    }

    private void startAnimation(final long currentPlayTime) {
        mTransformValueAnimator = new AnimatorValue();
        mTransformValueAnimator.setDuration(mTimerDuration);
        mTransformValueAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                if (mbViewVisible) {
                    mFactor = Math.round(value * getWidth() / 2);
                    // re-draw view as per the new calculated factor
                    invalidate();
                    checkOnTimeElapsedTime(value, mTimerDuration);
                }
            }
        });
        mTransformValueAnimator.start();
        setTimerElapsed(false);
    }

    private void checkOnTimeElapsedTime(float value, long mTimerDuration) {
        long elapsedTime = Math.round(value * mTimerDuration);
        if (elapsedTime >= mTimerDuration) {
            // timer has elapsed.
            setTimerElapsed(true);
        }

        if (mOnTimerElapsedListener != null) {
            mOnTimerElapsedListener.onTimeElapsed(elapsedTime, mTimerDuration);
        }
    }

    private void startResetAnimation(final OnTimerResetListener listener) {
        // stop timer if it is currently running
        if (mTransformValueAnimator != null && mTransformValueAnimator.isRunning()) {
            mTransformValueAnimator.cancel();
        }

        mTransformValueAnimator = new AnimatorValue();
        mTransformValueAnimator.setDuration(RESET_ANIM_DURATION);

        mTransformValueAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                if (mbViewVisible) {
                    mFactor = 0;
                    // re-draw view as per the new calculated factor
                    invalidate();
                }
            }
        });

        mTransformValueAnimator.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                setTimerElapsed(true);
                setTimerDuration(0);
                mCurrentPlayTime = 0;
                mFactor = 0;
                if (listener != null) {
                    listener.onTimerResetCompleted();
                }
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        mTransformValueAnimator.start();
    }

    /**
     * Start the timer for passed in duration. The timer bar will shrink in provided duration and will invoke
     * @param duration long duration this timer will run for.
     */
    public void start(final long duration) {
        if (!isRunning()) {
            if (mFactor == 0) {
                setTimerDuration(duration);
                startAnimation(0);
            } else {
                resetTimerListener(duration);
            }
        } else {
            LogUtil.error(TAG, new IllegalStateException("Timer is already running.").toString());
        }
    }

    private void resetTimerListener(long duration) {
        reset(new OnTimerResetListener() {
            @Override
            public void onTimerResetCompleted() {
                start(duration);
            }
        });
    }

    /**
     * Reset the timer for repeated usage.
     * @param listener {@link OnTimerResetListener}
     *
     */
    public void reset(final OnTimerResetListener listener) {
        startResetAnimation(listener);
    }

    /**
     * Start the timer for passed in duration. The timer bar will shrink in provided duration and will invoke
     *
     * @param duration long duration this timer will run for.
     * @param listener @{@link OnTimerElapsedListener} listener to get callback
     *                 once the @{@link BndrsntchTimer} elaspses.
     */
    public void start(final long duration, final OnTimerElapsedListener listener) {
        setTimerDuration(duration);
        setOnTimerElapsedListener(listener);
        start(duration);
    }

    /**
     * Register a callback to be invoked when {@link BndrsntchTimer} is elapsed.
     *
     * @param onTimerElapsedListener {@link OnTimerElapsedListener}
     */
    public void setOnTimerElapsedListener(final OnTimerElapsedListener onTimerElapsedListener) {
        mOnTimerElapsedListener = onTimerElapsedListener;
    }

    /**
     * Returns true of the timer is still running, else false
     *
     * @return boolean true of the timer is still running, else false
     */
    public boolean isRunning() {
        return !mbTimerElapsed;
    }


    @Override
    public void onDraw(Component component, Canvas canvas) {
        onMeasure();
        onSizeChanged();

        mLeftXPosition = getPaddingLeft() + mFactor;
        mRectF.left = mLeftXPosition - DEFAULT_PADDING_FACTOR;
        mRectF.right = getWidth() - getPaddingRight() - mFactor + DEFAULT_PADDING_FACTOR;
        mRectF.top = getPaddingTop() - DEFAULT_PADDING_FACTOR;
        mRectF.bottom = mRectF.top + mTimerHeight - getPaddingBottom() + DEFAULT_PADDING_FACTOR;

        canvas.drawRoundRect(mRectF, mRoundRectRadius, mRoundRectRadius, mBackgroundPaint);
    }

    private void onMeasure() {
        int desiredWidth = getEstimatedWidth();
        int desiredHeight = mTimerHeight;

        desiredWidth = desiredWidth + getPaddingLeft() + getPaddingRight();
        desiredHeight = desiredHeight + getPaddingTop() + getPaddingBottom();

        setComponentSize(measureDimension(desiredWidth, getWidth()), measureDimension(desiredHeight, getHeight()));
    }

    private void onSizeChanged() {
        // if animation was running, timer wasn't elapsed.
        if (mCurrentPlayTime != 0) {
            new EventHandler(EventRunner.getMainEventRunner()).postTask(new Runnable() {
                @Override
                public void run() {
                    startAnimation(mCurrentPlayTime);
                }
            });
        }
    }

    private int measureDimension(int desiredSize, int measureSpec) {
        int result = desiredSize;
        int specMode = EstimateSpec.getMode(measureSpec);
        int specSize = EstimateSpec.getSize(measureSpec);

        switch (specMode) {
            case EstimateSpec.PRECISE: {
                result = specSize;
                break;
            }
            case EstimateSpec.NOT_EXCEED: {
                result = Math.min(result, specSize);
                break;
            }
            case EstimateSpec.UNCONSTRAINT: {
                result = desiredSize;
                break;
            }
            default: {
                LogUtil.error(TAG, "measureDimension: Invalid Measure Spec.");
                break;
            }
        }

        if (result < desiredSize) {
            LogUtil.error(TAG, "The view is too small.");
        }
        return result;
    }

    /**
     * Returns a LifecycleObserver that expects to be notified when the LifecycleOwner changes state.
     * Add this as a {@link LifecycleObserver}
     *
     * @return LifecycleObserver
     */
    public BndrsntchTimer getLifecycleObserver() {
        return this;
    }

    @Override
    public void onStateChanged(Lifecycle.Event event, Intent intent) {
        switch (event) {
            case ON_START:
                mbViewVisible = true;

                // if animation was running, timer wasn't elapsed.

                if (mCurrentPlayTime != 0) {
                    startAnimation(mCurrentPlayTime);
                }
                return;

            case ON_STOP:
                mbViewVisible = false;
                if (mTransformValueAnimator != null && mTransformValueAnimator.isRunning()) {
                    mTransformValueAnimator.cancel();
                }
                return;
        }
    }
}